
#ifndef    HELP_LOGGER_HPP
#define    HELP_LOGGER_HPP

#include    <stdio.h>
#include    <sys/types.h>
#include    <sys/stat.h>
#include    <unistd.h>
#include    <time.h>
#include    <stdarg.h>
#include    <errno.h>

#include    <string>

namespace help
{

class    LogRecord;
class    LogLevel;

///////////////////////////////////////////////////////////////////////

class    LogRecord
{
public    :
    LogRecord(const char* pszTimestamp, int iLevel, const char* pszMessage, const char* pszFileName = NULL, int iLineNo = 0)
        :    m_pszTimestamp(pszTimestamp), m_iLevel(iLevel), m_pszMessage(pszMessage), m_pszFileName(pszFileName), m_iLineNo(iLineNo)
    {}
            
public    :
    const char*    timestamp(void) const { return m_pszTimestamp; }
    int            level(void)     const { return m_iLevel; }
    const char*    message(void)   const { return m_pszMessage; }
    const char*    filename(void)  const { return m_pszFileName; }
    int            lineno(void)    const { return m_iLineNo; }
                
private    :
    const char*    m_pszTimestamp;
    int    m_iLevel;
    const char*    m_pszMessage;
    const char*    m_pszFileName;
    int    m_iLineNo;
};

class    LogLevel
{
public    :

    enum {
        ERROR = 1,
        INFO  = 2,
        DEBUG = 3,
        ALL = 4
    };
    
    static  const char*   name(int iLevel)
    {
        static    const char*    s_ppszName[] = {
            "UNKNOWN", "ERROR", "INFO", "DEBUG", "ALL",
        };    
        
        if ((iLevel < ERROR) || (iLevel >= ALL)) {
            return    s_ppszName[0];
        }
        else {
            return    s_ppszName[iLevel];
        }
    }
    
private    :
    LogLevel();
    
};

///////////////////////////////////////////////////////////////////////

class   LogAppender
{
public  :
    virtual int append(const LogRecord&) = 0;
};

class   ConsoleLogAppender  :   public  LogAppender
{
public  :
    ConsoleLogAppender()
        :    m_pFileError(stderr), m_pFileInfo(stdout), m_pFileDebug(stdout)
    {
    }
    
public  :
    virtual int append(const LogRecord& record)
    {
       if ( LogLevel::ERROR == record.level() ) {
            if ( NULL == record.filename() ) {
                fprintf(m_pFileError, "%s [%s] %s\n", record.timestamp(), LogLevel::name(record.level()), record.message());
            } else {
                fprintf(m_pFileError, "%s %s:%d [%s] %s\n", record.timestamp(), record.filename(), record.lineno(), LogLevel::name(record.level()), record.message());
            }
        }
        else if ( LogLevel::INFO == record.level() ) {
            if ( NULL == record.filename() ) {
                fprintf(m_pFileInfo, "%s [%s] %s\n", record.timestamp(), LogLevel::name(record.level()), record.message());
            } else {
                fprintf(m_pFileInfo, "%s %s:%d [%s] %s\n", record.timestamp(), record.filename(), record.lineno(), LogLevel::name(record.level()), record.message());
            }
        }
        else if ( LogLevel::DEBUG == record.level() ) {
            if ( NULL == record.filename() ) {
                fprintf(m_pFileDebug, "%s [%s] %s\n", record.timestamp(), LogLevel::name(record.level()), record.message());
            } else {
                fprintf(m_pFileDebug, "%s %s:%d [%s] %s\n", record.timestamp(), record.filename(), record.lineno(), LogLevel::name(record.level()), record.message());
            }
        }

        return  0;
    }

private    :
    FILE*    m_pFileError;
    FILE*    m_pFileInfo;
    FILE*    m_pFileDebug;
};

class   FileLogAppender :   public  LogAppender
{
    #define LOG_FILE_ERROR  0x0701
    #define LOG_FILE_INFO   0x0702
    #define LOG_FILE_DEBUG  0x0703
    
    #define LOG_FILE_SIZE   (32 * 1048576)
    
public  :
    FileLogAppender(const char* pszPathPrefix="/tmp/zxant")
        :    m_pFileError(stderr), m_pFileInfo(stdout), m_pFileDebug(stdout),
            m_strPathPrefix(pszPathPrefix)
    {
        char    szPath[512];
        strncpy(szPath, pszPathPrefix, sizeof(szPath) - 1);
        int     i = strlen(szPath);
        for(;i>0;--i) {
            if ( '/' == szPath[i] ) {
                szPath[i] = '\0';
                break;
            }
        }
        int iReturn = makedir(szPath);
        if (iReturn < 0) {
            return;
        }
            
    }
    
    ~FileLogAppender()
    {
        if (stderr != m_pFileError) {
            fclose(m_pFileError);
        }
        if (stdout != m_pFileInfo) {
            fclose(m_pFileInfo);
        }
        if (stdout != m_pFileDebug) {
            fclose(m_pFileDebug);
        }
    }
    
public  :
    virtual int append(const LogRecord& record)
    {
       if ( LogLevel::ERROR == record.level() ) {
            if ((NULL == m_pFileError) || (stderr == m_pFileError)) {
                m_pFileError = this->openLogFile(LOG_FILE_ERROR);
            }
            if ( NULL == record.filename() ) {
                fprintf(m_pFileError, "%s [%s] %s\n", record.timestamp(), LogLevel::name(record.level()), record.message());
            } else {
                fprintf(m_pFileError, "%s %s:%d [%s] %s\n", record.timestamp(), record.filename(), record.lineno(), LogLevel::name(record.level()), record.message());
            }
            if ((stderr != m_pFileError) && (ftell(m_pFileError) > LOG_FILE_SIZE)) {
                fclose(m_pFileError);
                m_pFileError = stderr;
            }
        }
        else if ( LogLevel::INFO == record.level() ) {
            if ((NULL == m_pFileInfo) || (stdout == m_pFileInfo)) {
                m_pFileInfo = this->openLogFile(LOG_FILE_INFO);
            }
            if ( NULL == record.filename() ) {
                fprintf(m_pFileInfo, "%s [%s] %s\n", record.timestamp(), LogLevel::name(record.level()), record.message());
            } else {
                fprintf(m_pFileInfo, "%s %s:%d [%s] %s\n", record.timestamp(), record.filename(), record.lineno(), LogLevel::name(record.level()), record.message());
            }
            if ((stdout != m_pFileInfo) && (ftell(m_pFileInfo) > LOG_FILE_SIZE)) {
                fclose(m_pFileInfo);
                m_pFileInfo = stdout;
            }
        }
        else if ( LogLevel::DEBUG == record.level() ) {
            if ((NULL == m_pFileDebug) || (stdout == m_pFileDebug)) {
                m_pFileDebug = this->openLogFile(LOG_FILE_DEBUG);
            }
            if ( NULL == record.filename() ) {
                fprintf(m_pFileDebug, "%s [%s] %s\n", record.timestamp(), LogLevel::name(record.level()), record.message());
            } else {
                fprintf(m_pFileDebug, "%s %s:%d [%s] %s\n", record.timestamp(), record.filename(), record.lineno(), LogLevel::name(record.level()), record.message());
            }
            if ((stdout != m_pFileDebug) && (ftell(m_pFileDebug) > LOG_FILE_SIZE)) {
                fclose(m_pFileDebug);
                m_pFileDebug = stdout;
            }
        }

        return  0;
    }

private :
    FILE*   openLogFile( int iLogFileType )
    {
        FILE*   defaultFile = stderr;
        if (LOG_FILE_ERROR != iLogFileType) {
            defaultFile = stdout;
        }
        
        const char* pszTag = NULL;
        switch(iLogFileType) {
        case    LOG_FILE_ERROR  :
            pszTag = "error";
            break;
        case    LOG_FILE_INFO   :
            pszTag = "info";
            break;
        case    LOG_FILE_DEBUG  :
            pszTag = "debug";
            break;
        default :
            pszTag = "ants";
            break;
        };
        
        char    szFileName[ 256 ];
        if ( m_strPathPrefix.size() < 1 ) {
            return  defaultFile;     
        }
        else {
            int iLength = snprintf( szFileName, sizeof(szFileName) - 1, "%s.%s.log", m_strPathPrefix.c_str(), pszTag );
            szFileName[ iLength ] = '\0';   
        }
        
        FILE*   pFile = fopen( szFileName, "w" );
        if ( NULL == pFile ) {
            pFile = defaultFile;
        }
        else {
            fclose( pFile );
            pFile = fopen( szFileName, "a" );
            setbuf( pFile, NULL );
        }
        
        return  pFile;
    }

    int makedir ( const char* pszPath )
    {
        char    szPath [ 1024 ];
        const char* p = pszPath;
        
        memset ( szPath, 0, sizeof(szPath) );
        
        while (*p)
        {
            if ('/' == *p)
            {
                if (pszPath == p)
                {
                    // root dir
                    // skip it
                    ++p;
                    continue;
                }
            
                strncpy ( szPath, pszPath, p - pszPath );
                if (0 == ::access( szPath, F_OK))
                {
                    // directory or file exists
                    ++p;
                    continue;
                }
                
                // directory doesNOT exists
                int iReturn = ::mkdir ( szPath, 0777 );
                if (iReturn < 0)
                {
                    fprintf(stderr, "Fail to mkdir '%s' for path '%s'", szPath, pszPath );
                    return  -1;
                }
            }
            
            ++p;
        }
        
        if (0 != ::access( pszPath, F_OK))
        {
            // directory doesNOT exists
            int iReturn = ::mkdir ( pszPath, 0777 );
            if (iReturn < 0)
            {
                fprintf(stderr, "Fail to mkdir '%s' : %s", pszPath, strerror(errno) );
                return  -1;
            }
        }
        
        return  0;
    }

private    :
    FILE*    m_pFileError;
    FILE*    m_pFileInfo;
    FILE*    m_pFileDebug;
    
    std::string m_strPathPrefix;
};

///////////////////////////////////////////////////////////////////////


class    Logger
{

public    :
    ~Logger()
    {
        if ( NULL != m_pAppender ) {
            delete m_pAppender;
        }
    }

public    :

    void    error(const char* pszFile, int iLineNo, const char* pszFormat, ... )
    {
        char        szTimestamp[32];
        char        szMessage[256];
        getTimeString(szTimestamp, sizeof(szTimestamp));
        
        va_list     args;
        va_start(args, pszFormat);
        int iLength = vsnprintf(szMessage, sizeof(szMessage) - 1, pszFormat, args);
        va_end(args);
        szMessage[iLength] = '\0';
        
        LogRecord    record(szTimestamp, LogLevel::ERROR, szMessage, pszFile, iLineNo);
        this->append(record);
    }
        
    void    error(const char* pszFormat, ... )
    {
        char        szTimestamp[32];
        char        szMessage[256];
        getTimeString(szTimestamp, sizeof(szTimestamp));
        
        va_list     args;
        va_start(args, pszFormat);
        int iLength = vsnprintf(szMessage, sizeof(szMessage) - 1, pszFormat, args);
        va_end(args);
        szMessage[iLength] = '\0';
        
        LogRecord    record(szTimestamp, LogLevel::ERROR, szMessage);
        this->append(record);
    }
        
    void    info(const char* pszFile, int iLineNo, const char* pszFormat, ... )
    {
        char        szTimestamp[32];
        char        szMessage[256];
        getTimeString(szTimestamp, sizeof(szTimestamp));
        
        va_list     args;
        va_start(args, pszFormat);
        int iLength = vsnprintf(szMessage, sizeof(szMessage) - 1, pszFormat, args);
        va_end(args);
        szMessage[iLength] = '\0';
        
        LogRecord    record(szTimestamp, LogLevel::INFO, szMessage, pszFile, iLineNo);
        this->append(record);
    }
        
    void    info(const char* pszFormat, ... )
    {
        char        szTimestamp[32];
        char        szMessage[256];
        getTimeString(szTimestamp, sizeof(szTimestamp));
        
        va_list     args;
        va_start(args, pszFormat);
        int iLength = vsnprintf(szMessage, sizeof(szMessage) - 1, pszFormat, args);
        va_end(args);
        szMessage[iLength] = '\0';
        
        LogRecord    record(szTimestamp, LogLevel::INFO, szMessage);
        this->append(record);
    }
        
    void    debug(const char* pszFile, int iLineNo, const char* pszFormat, ... )
    {
        if ( !debugable() ) {
            return;
        }
        
        char        szTimestamp[32];
        char        szMessage[256];
        getTimeString(szTimestamp, sizeof(szTimestamp));
        
        va_list     args;
        va_start(args, pszFormat);
        int iLength = vsnprintf(szMessage, sizeof(szMessage) - 1, pszFormat, args);
        va_end(args);
        szMessage[iLength] = '\0';
        
        LogRecord    record(szTimestamp, LogLevel::DEBUG, szMessage, pszFile, iLineNo);
        this->append(record);
    }
        
    void    debug(const char* pszFormat, ... )
    {
        if ( !debugable() ) {
            return;
        }
        
        char        szTimestamp[32];
        char        szMessage[256];
        getTimeString(szTimestamp, sizeof(szTimestamp));
        
        va_list     args;
        va_start(args, pszFormat);
        int iLength = vsnprintf(szMessage, sizeof(szMessage) - 1, pszFormat, args);
        va_end(args);
        szMessage[iLength] = '\0';
        
        LogRecord    record(szTimestamp, LogLevel::DEBUG, szMessage);
        this->append(record);
    }

private    :

    void    append( const LogRecord& record )
    {
        if (NULL != m_pAppender) {
            m_pAppender->append(record);
        }
    }
            
public  :
    void    setLogLevelThreshold(int iLevel)
    {
        m_iLogLevelThreshold = iLevel;
    }
    
    bool    debugable(void) const 
    {
        return  (m_iLogLevelThreshold >= LogLevel::DEBUG);
    }
    
public  :
    void    useFileAppender(const char* pszPathPrefix="/tmp/zxant")
    {
        if (NULL != m_pAppender) {
            delete m_pAppender;
            m_pAppender = NULL;
        }
        
        m_pAppender = new FileLogAppender(pszPathPrefix);
    }
    
    void    useConsoleAppender()
    {
        if (NULL != m_pAppender) {
            delete m_pAppender;
            m_pAppender = NULL;
        }
        
        m_pAppender = new ConsoleLogAppender();
    }
    
public    :

    static  Logger&  getInstance(void)
    {
        static    Logger*    s_pLogger = NULL;
        
        if (NULL == s_pLogger) {
            s_pLogger = new Logger();
        }
        
        return    *s_pLogger;
    }
    
    static    int     getTimeString ( char* pszBuf, size_t iBufSize )
    {
        struct tm*  tmp;
        time_t  t;
        int     iReturn;
        
        pszBuf[0] = '\0';
        
        t = time (NULL);
        tmp = localtime( &t );
        if ( NULL == tmp ) {
            return  -1;
        }
        
        pszBuf[0] = '\0';
        iReturn = strftime(pszBuf, iBufSize, "%Y-%m-%d %H:%M:%S", tmp);
        if (iReturn > 0) {
            pszBuf[ iReturn ] = '\0';
        }
        
        return  0;
    }   

private    :
    Logger(const char* pszConfigure = NULL)
        :    m_pAppender(NULL),
            m_iLogLevelThreshold(LogLevel::INFO)
    {
        m_pAppender = new ConsoleLogAppender();
    }
    
private    :
    LogAppender*    m_pAppender;
        
    int     m_iLogLevelThreshold;
};

}    // namespace help

#endif
